// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: junglekim

#pragma once

#include "coin2/strategy/vmm/vmm_pricer.h"
#include "coin2/strategy/vmm/vmm_util.h"

namespace coin2::strategy::vmm {

class VmmBboPricer : public VmmPricer {
 public:
  VmmBboPricer(const ::nlohmann::json& configs, const bool& verbose) : VmmPricer(configs, verbose) {
    for (auto& config : configs_.items()) {
      std::string symbol = config.key();
      bbos_[symbol] = Bbo();
    }
  }

  void onBookFeed(
      const FeedUpdate& upd,
      const int64_t& timestamp,
      const std::unordered_map<std::string, VmmPriceResult>& prev_prices) final {
    std::string symbol = GetSymbol(upd.product());

    auto book = upd.GetBookBuilder();
    auto ask0p = book->Ask0()->price;
    auto ask0q = book->Ask0()->qty;
    auto bid0p = book->Bid0()->price;
    auto bid0q = book->Bid0()->qty;
    double midp = (ask0p + bid0p) * 0.5;

    if (!upd.book().Ask0().has_value() || !upd.book().Bid0().has_value()) {
      SPDLOG_ERROR("[BOOK FEED PRICE NO VALUE!] {} {} {}", symbol, ask0p, bid0p);
      result_.clear();
      return;
    }

    midp_map_[symbol] = midp;

    Bbo bbo;
    bbo.symbol = symbol;
    bbo.ask0p = ask0p;
    bbo.ask0q = ask0q;
    bbo.bid0p = bid0p;
    bbo.bid0q = bid0q;
    bbo.midp = midp;
    bbo.timestamp = timestamp;
    bbos_[symbol].CopyAndIncSeqNo(bbo);
    if (verbose_)
      SPDLOG_INFO("VmmBboPricer: {} bid: {} ask: {} midp: {}", symbol, bid0p, ask0p, midp);
    for (const auto& [sym, bbo] : bbos_) {
      if (!bbo.is_ready()) {
        SPDLOG_INFO("BBO {} NOT READY !@#$!", sym);
        result_.clear();
        return;
      }
    }

    result_.clear();
    UpdateResult(symbol, {book, {bid0p, bid0q}, {ask0p, ask0q}, {bid0p, bid0q}, {ask0p, ask0q}});
  }
};

class VmmBboReferencePricer : public VmmPricer {
 public:
  VmmBboReferencePricer(const ::nlohmann::json& configs, const bool& verbose)
      : VmmPricer(configs, verbose) {
    for (auto& config : configs_.items()) {
      std::string symbol = config.key();
      bbos_[symbol] = Bbo();
      result_[symbol] = VmmPriceResult();
      prev_result_[symbol] = VmmPriceResult();
      if (config.value().contains("ref_price")) {
        priced_ref_prices_[symbol] = {};
        priced_ref_midp_[symbol] = {};
        ref_configs_[symbol] = {};
        for (const auto& ref_info : config.value()["ref_price"]) {
          if (ref_info.contains("base_ref")) {
            std::unordered_map<std::string, bool> quote_ref_symbols;
            for (const auto& [quote_symbol, is_quote_multiply] : ref_info["quote_refs"].items()) {
              quote_ref_symbols[quote_symbol] = is_quote_multiply;
              ref_prices_[quote_symbol] = {std::nullopt, std::nullopt};
              if (ref_sym_map_.find(quote_symbol) == ref_sym_map_.end()) {
                ref_sym_map_[quote_symbol] = {symbol};
              } else {
                ref_sym_map_[quote_symbol].insert(symbol);
              }
            }
            ref_configs_[symbol].push_back({ref_info["base_ref"], quote_ref_symbols});
            ref_prices_[ref_info["base_ref"]] = {std::nullopt, std::nullopt};
            priced_ref_prices_[symbol].push_back({std::nullopt, std::nullopt});
            priced_ref_midp_[symbol].push_back(std::nullopt);
            if (ref_sym_map_.find(ref_info["base_ref"]) == ref_sym_map_.end()) {
              ref_sym_map_[ref_info["base_ref"]] = {symbol};
            } else {
              ref_sym_map_[ref_info["base_ref"]].insert(symbol);
            }
          }
        }
      }
      if (config.value().contains("use_ref_midp")) {
        use_ref_midp_[symbol] = true;
        midp_premium_bp_[symbol] = config.value()["use_ref_midp"]["midp_premium_bp"].get<double>();
      } else {
        use_ref_midp_[symbol] = false;
      }
      if (config.value().contains("is_no_arbitrage")) {
        is_no_arbitrage_[symbol] = config.value()["is_no_arbitrage"].get<bool>();
        CHECK(is_no_arbitrage_[symbol]);
      }
    }
  }
  void onAccountOrder(const OrderUpdate& upd) {}

  void UpdatePricing(const std::string& target_symbol) {
    auto& vmm_price_result = result_[target_symbol];
    auto& prev_vmm_price_result = prev_result_[target_symbol];
    std::optional<double> priced_bid{std::nullopt}, priced_ask{std::nullopt},
        priced_midp{std::nullopt};
    if (is_no_arbitrage_[target_symbol]) {
      for (const auto& [priced_ref_bid, priced_ref_ask] : priced_ref_prices_[target_symbol]) {
        if (priced_ref_bid.has_value() && priced_ref_ask.has_value() && !priced_bid.has_value() &&
            !priced_ask.has_value()) {
          priced_bid = priced_ref_bid.value();
          priced_ask = priced_ref_ask.value();
          continue;
        }
        if (priced_bid.has_value() && priced_ref_bid.has_value() &&
            priced_bid.value() <= priced_ref_bid.value()) {
          priced_bid.value() = priced_ref_bid.value();
        }
        if (priced_ask.has_value() && priced_ref_ask.has_value() &&
            priced_ask.value() >= priced_ref_ask.value()) {
          priced_ask.value() = priced_ref_ask.value();
        }
      }
    }
    if (use_ref_midp_[target_symbol]) {
      int denom = 0;
      for (const auto& priced_ref_midp : priced_ref_midp_[target_symbol]) {
        if (priced_ref_midp.has_value() && !priced_midp.has_value()) {
          priced_midp = priced_ref_midp.value();
          denom++;
          continue;
        }
        if (priced_midp.has_value() && priced_ref_midp.has_value()) {
          priced_midp.value() += priced_ref_midp.value();
          denom++;
        }
      }
      if (denom > 0) {
        priced_midp = priced_midp.value() / denom;
        const double premium_ref_priced_midp =
            priced_midp.value() * (1 + midp_premium_bp_[target_symbol] * 1e-4);
        vmm_price_result.ref_priced_midp = premium_ref_priced_midp;
        prev_vmm_price_result.ref_priced_midp = premium_ref_priced_midp;
        if (verbose_) SPDLOG_INFO("ref_priced_midp: {}", vmm_price_result.ref_priced_midp.value());
      }
    }
    vmm_price_result.priced_bid.first = priced_bid;
    vmm_price_result.priced_ask.first = priced_ask;
    prev_vmm_price_result.priced_bid.first = priced_bid;
    prev_vmm_price_result.priced_ask.first = priced_ask;
  }

  void UpdateResultAndRef(
      std::optional<std::string> ref_symbol,
      std::optional<std::string> symbol) {
    // ref pricing change -> change related trading symbol's min/max range
    if (ref_symbol.has_value()) {
      const auto& target_symbols = ref_sym_map_[ref_symbol.value()];
      for (const auto& target_symbol : target_symbols) {
        UpdatePricing(target_symbol);
      }
      return;
    }
    // trading symbol's price change -> Just check with current ref prices
    if (symbol.has_value()) {
      auto& vmm_price_result = result_[symbol.value()];
      auto& prev_vmm_price_result = prev_result_[symbol.value()];
      std::optional<double> max_ref_bid{std::nullopt}, min_ref_ask{std::nullopt},
          priced_midp{std::nullopt};
      UpdatePricing(symbol.value());
      if (is_no_arbitrage_[symbol.value()]) {
        for (const auto& [priced_ref_bid, priced_ref_ask] : priced_ref_prices_[symbol.value()]) {
          if (priced_ref_bid.has_value() && priced_ref_ask.has_value() &&
              !max_ref_bid.has_value() && !min_ref_ask.has_value()) {
            max_ref_bid = priced_ref_bid.value();
            min_ref_ask = priced_ref_ask.value();
            continue;
          }
          if (max_ref_bid.has_value() && priced_ref_bid.has_value() &&
              max_ref_bid.value() <= priced_ref_bid.value()) {
            max_ref_bid.value() = priced_ref_bid.value();
          }
          if (min_ref_ask.has_value() && priced_ref_ask.has_value() &&
              min_ref_ask.value() >= priced_ref_ask.value()) {
            min_ref_ask.value() = priced_ref_ask.value();
          }
        }
      }
      if (use_ref_midp_[symbol.value()]) {
        int denom = 0;
        for (const auto& priced_ref_midp : priced_ref_midp_[symbol.value()]) {
          if (priced_ref_midp.has_value() && !priced_midp.has_value()) {
            priced_midp = priced_ref_midp.value();
            denom++;
            continue;
          }
          if (priced_midp.has_value() && priced_ref_midp.has_value()) {
            priced_midp.value() += priced_ref_midp.value();
            denom++;
          }
        }
        if (denom > 0) {
          priced_midp = priced_midp.value() / denom;
          vmm_price_result.ref_priced_midp = priced_midp;
          prev_vmm_price_result.ref_priced_midp = priced_midp;
        }
      }
      vmm_price_result.priced_bid.first = max_ref_bid;
      vmm_price_result.priced_ask.first = min_ref_ask;
      prev_vmm_price_result.priced_bid.first = max_ref_bid;
      prev_vmm_price_result.priced_ask.first = min_ref_ask;
      return;
    }
  }

  void UpdateRefPrice(const std::string& ref_symbol) {
    const auto& target_symbols = ref_sym_map_[ref_symbol];
    for (const auto& symbol : target_symbols) {
      const auto& ref_config_vector = ref_configs_[symbol];
      if (verbose_) SPDLOG_INFO("UpdateRefPrice: {}", symbol);
      for (int i = 0; i < ref_config_vector.size(); ++i) {
        const auto& ref_config = ref_config_vector[i];
        std::optional<double> priced_bid{std::nullopt}, priced_ask{std::nullopt},
            priced_midp{std::nullopt};
        const auto& [base_bid, base_ask] = ref_prices_[ref_config.first];
        if (base_bid.has_value() && base_ask.has_value()) {
          if (verbose_)
            SPDLOG_INFO(
                "BaseSymbol: {} Bid: {} Ask: {}",
                ref_config.first,
                base_bid.value(),
                base_ask.value());
          if (ref_config.second.empty()) {
            priced_bid = base_bid.value();
            priced_ask = base_ask.value();
            priced_midp = (base_bid.value() + base_ask.value()) / 2.0;
          } else {
            double quote_bid_mult{1.0}, quote_ask_mult{1.0}, quote_midp_mult{1.0};
            bool is_breaked{false};
            for (const auto& [quote_symbol, is_quote_multiply] : ref_config.second) {
              const auto& [quote_bid, quote_ask] = ref_prices_[quote_symbol];
              if (quote_bid.has_value() && quote_ask.has_value()) {
                if (verbose_)
                  SPDLOG_INFO(
                      "QuoteSymbol: {} Bid: {} Ask: {}",
                      quote_symbol,
                      quote_bid.value(),
                      quote_ask.value());
                if (is_quote_multiply) {                // PRICING = BASE * QUOTE
                  quote_ask_mult *= quote_ask.value();  // PRICING_ASK = BASE_ASK * QUOTE_ASK
                  quote_bid_mult *= quote_bid.value();  // PRICING_BID = BASE_BID * QUOTE_BID
                  quote_midp_mult *= (quote_bid.value() + quote_ask.value()) / 2.0;
                } else {                                // PRICING = BASE / QUOTE
                  quote_ask_mult /= quote_bid.value();  // PRICING_ASK = BASE_ASK / QUOTE_BID
                  quote_bid_mult /= quote_ask.value();  // PRICING_BID = BASE_BID / QUOTE_ASK
                  quote_midp_mult /= (quote_bid.value() + quote_ask.value()) / 2.0;
                }
              } else {  // If one of quote_symbol doesn't have price : break and no pricing
                if (verbose_) SPDLOG_INFO("QuoteSymbol: {} doesn't have price", quote_symbol);
                is_breaked = true;
                break;
              }
            }
            if (is_breaked) {
              if (verbose_) SPDLOG_INFO("Breaked");
              continue;
            }
            priced_bid = base_bid.value() * quote_bid_mult;
            priced_ask = base_ask.value() * quote_ask_mult;
            priced_midp = (base_bid.value() + base_ask.value()) / 2.0 * quote_midp_mult;
            if (verbose_) {
              SPDLOG_INFO(
                  "BaseSymbol: {} Bid: {} Ask: {} QuoteBidMult: {} QuoteAskMult: {}",
                  ref_config.first,
                  base_bid.value(),
                  base_ask.value(),
                  quote_bid_mult,
                  quote_ask_mult);
              SPDLOG_INFO(
                  "PricedSymbol: {} Bid: {} Ask: {}",
                  symbol,
                  priced_bid.value(),
                  priced_ask.value());
            }
          }
        }
        priced_ref_prices_[symbol][i] = {priced_bid, priced_ask};
        priced_ref_midp_[symbol][i] = priced_midp;
      }
    }
    UpdateResultAndRef(std::optional<std::string>(ref_symbol), std::nullopt);
  }

  void onBookFeed(
      const FeedUpdate& upd,
      const int64_t& timestamp,
      const std::unordered_map<std::string, VmmPriceResult>& prev_prices) final {
    const std::string symbol = GetSymbol(upd.product());
    auto book = upd.GetBookBuilder();
    auto ask0p = book->Ask0()->price;
    auto ask0q = book->Ask0()->qty;
    auto bid0p = book->Bid0()->price;
    auto bid0q = book->Bid0()->qty;
    double midp = (ask0p + bid0p) * 0.5;

    if (!upd.book().Ask0().has_value() || !upd.book().Bid0().has_value()) {
      SPDLOG_ERROR("[BOOK FEED PRICE NO VALUE!] {} {} {}", symbol, ask0p, bid0p);
      result_.clear();
      return;
    }
    if (verbose_)
      SPDLOG_INFO("VmmBboReferencePricer: {} bid: {} ask: {} midp: {}", symbol, bid0p, ask0p, midp);
    if (ref_prices_.find(symbol) != ref_prices_.end()) {
      ref_prices_[symbol] = {bid0p, ask0p};
      UpdateRefPrice(symbol);
      return;
    }

    midp_map_[symbol] = midp;

    Bbo bbo;
    bbo.symbol = symbol;
    bbo.ask0p = ask0p;
    bbo.ask0q = ask0q;
    bbo.bid0p = bid0p;
    bbo.bid0q = bid0q;
    bbo.midp = midp;
    bbo.timestamp = timestamp;
    bbos_[symbol].CopyAndIncSeqNo(bbo);

    for (const auto& [sym, bbo] : bbos_) {
      if (!bbo.is_ready()) {
        SPDLOG_INFO("BBO {} NOT READY !@#$!", sym);
        result_.clear();
        return;
      }
    }
    UpdateResult(symbol, {book, {bid0p, bid0q}, {ask0p, ask0q}, {bid0p, bid0q}, {ask0p, ask0q}});
    UpdateResultAndRef(std::nullopt, symbol);
  }

 private:
  std::unordered_map<
      std::string,
      std::vector<std::pair<std::string, std::unordered_map<std::string, bool>>>>
      ref_configs_;
  std::unordered_map<std::string, std::set<std::string>> ref_sym_map_;
  std::unordered_map<std::string, std::pair<std::optional<double>, std::optional<double>>>
      ref_prices_;
  std::unordered_map<
      std::string,
      std::vector<std::pair<std::optional<double>, std::optional<double>>>>
      priced_ref_prices_;
  std::unordered_map<std::string, std::vector<std::optional<double>>> priced_ref_midp_;
  std::unordered_map<std::string, bool> use_ref_midp_;
  std::unordered_map<std::string, double> midp_premium_bp_;
  std::unordered_map<std::string, bool> is_no_arbitrage_;
};

}  // namespace coin2::strategy::vmm